/*********************************************************************
 * File Name: newchrled.c
 * Summary: Advanced character device driver with dynamic device management
 * Description: 
 * This file implements an advanced character device driver for LED control
 * with dynamic device number allocation and modern Linux kernel features.
 *
 * Key Features:
 * - Dynamic device number allocation (alloc_chrdev_region)
 * - Character device registration with cdev structure
 * - Device class and device node creation
 * - GPIO register mapping and LED control
 * - User space to kernel space data transfer
 * - Modern Linux kernel driver architecture
 *
 * Modification History:
 * Version    Author      Date        Changes
 *--------------------------------------------------
 * 1.00.00    yushuang    2025-01-11  Initial release
 *--------------------------------------------------
 ********************************************************************/

#include <linux/types.h>   /**< ssize_t, size_t, loff_t, __user */
#include <linux/kernel.h>  /**< printk, memcpy, error codes */
#include <linux/delay.h>   /**< udelay, mdelay */
#include <linux/ide.h>     /**< IDE related definitions */
#include <linux/init.h>    /**< __init, __exit, module_init, module_exit */
#include <linux/module.h>  /**< THIS_MODULE, MODULE_LICENSE, MODULE_AUTHOR */
#include <linux/errno.h>   /**< Error codes */
#include <linux/gpio.h>    /**< GPIO related definitions */
#include <linux/cdev.h>    /**< Character device structure */
#include <linux/device.h>  /**< Device class and device creation */

#include <asm/mach/map.h>  /**< Memory mapping functions */
#include <asm/uaccess.h>  /**< User space access functions */
#include <asm/io.h>       /**< I/O access functions */

#define NEWCHRLED_CNT      1           /**< Number of device instances */
#define NEWCHRLED_NAME     "newchrled" /**< Device name */
#define LEDOFF             0           /**< LED off state */
#define LEDON              1           /**< LED on state */

/**< Physical register addresses */
#define CCM_CCGR1_BASE             (0X020C406C)    /**< Clock control register base */
#define SW_MUX_GPIO1_IO03_BASE     (0X020E0068)    /**< GPIO1_IO03 mux register base */
#define SW_PAD_GPIO1_IO03_BASE     (0X020E02F4)    /**< GPIO1_IO03 pad register base */
#define GPIO1_DR_BASE              (0X0209C000)    /**< GPIO1 data register base */
#define GPIO1_GDIR_BASE            (0X0209C004)    /**< GPIO1 direction register base */

/**< Virtual address pointers for mapped registers */
static void __iomem *IMX6U_CCM_CCGR1;      /**< Clock control register virtual address */
static void __iomem *SW_MUX_GPIO1_IO03;    /**< GPIO1_IO03 mux register virtual address */
static void __iomem *SW_PAD_GPIO1_IO03;    /**< GPIO1_IO03 pad register virtual address */
static void __iomem *GPIO1_DR;              /**< GPIO1 data register virtual address */
static void __iomem *GPIO1_GDIR;            /**< GPIO1 direction register virtual address */

/* Device structure */
struct newchrled_dev
{
    dev_t devid;            /* Device number */
    struct cdev cdev;       /* Character device */
    struct class *class;    /* Device class */
    struct device *device;  /* Device instance */
    int major;              /* Major number */
    int minor;              /* Minor number */
};

struct newchrled_dev newchrled = {0};     /* LED device - initialize to zero */

/**
 * @brief LED switch control function
 * @param sta LED state: LEDON(1) to turn on LED, LEDOFF(0) to turn off LED
 * @return None
 */
void led_switch(u8 sta)
{
    u32 val = 0;
    if (sta == LEDON)
    {
        val = readl(GPIO1_DR);       /* Read current GPIO state */
        val &= ~(1 << 3);            /* Clear bit 3 (LED on = low) */
        writel(val, GPIO1_DR);
    }
    else if (sta == LEDOFF)
    {
        val = readl(GPIO1_DR);       /* Read current GPIO state */
        val |= (1 << 3);             /* Set bit 3 (LED off = high) */
        writel(val, GPIO1_DR);
    }
}

/**
 * @brief Open device function
 * @param inode Inode structure passed to driver
 * @param filp File structure, contains private_data member variable
 *              which is usually set to point to device structure during open
 * @return 0 on success, negative value on failure
 */
static int led_open(struct inode *inode, struct file *filp)
{
    filp->private_data = &newchrled; /* Set device pointer */
    return 0;
}

/**
 * @brief Read data from device
 * @param filp Device file to read from (file descriptor)
 * @param buf Buffer to return data to user space
 * @param cnt Number of bytes to read
 * @param offt Offset relative to file start address
 * @return Number of bytes read, negative value on failure
 */
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
    return 0;
}

/**
 * @brief Write data to device
 * @param filp Device file representing opened file descriptor
 * @param buf Data to write to device
 * @param cnt Number of bytes to write
 * @param offt Offset relative to file start address
 * @return Number of bytes written, negative value on failure
 */
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    int retvalue;
    unsigned char databuf[1];
    unsigned char ledstat;

    /* Limit write size to prevent buffer overflow */
    if (cnt > 1)
    {
        printk("Write size too large, max 1 byte\n");
        return -EINVAL;
    }

    retvalue = copy_from_user(databuf, buf, cnt);  /* Copy from user space */
    if (retvalue > 0)  /* copy_from_user returns number of bytes NOT copied */
    {
        printk("kernel write failed!\r\n");
        return -EFAULT;
    }

    ledstat = databuf[0];       /* Get LED control value */

    if (ledstat == LEDON)
    {
        led_switch(LEDON);      /* Turn on LED */
    }
    else if (ledstat == LEDOFF)
    {
        led_switch(LEDOFF);     /* Turn off LED */
    }
    return 0;
}

/**
 * @brief Close/release device function
 * @param inode Inode structure
 * @param filp Device file to close (file descriptor)
 * @return 0 on success, negative value on failure
 */
static int led_release(struct inode *inode, struct file *filp)
{
    return 0;
}

/* File operations */
static struct file_operations newchrled_fops = {
    .owner = THIS_MODULE,
    .open = led_open,
    .read = led_read,
    .write = led_write,
    .release = led_release,
};

/**
 * @brief Driver initialization function
 * @param None
 * @return None
 */
static int __init led_init(void)
{
    u32 val = 0;

    /* Initialize LED hardware */
    /* 1. Map physical registers to virtual addresses */
    IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
    if (!IMX6U_CCM_CCGR1)
    {
        printk("Failed to map CCM_CCGR1 register\n");
        return -ENOMEM;
    }
    
    SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
    if (!SW_MUX_GPIO1_IO03)
    {
        printk("Failed to map SW_MUX_GPIO1_IO03 register\n");
        iounmap(IMX6U_CCM_CCGR1);
        return -ENOMEM;
    }
    
    SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
    if (!SW_PAD_GPIO1_IO03)
    {
        printk("Failed to map SW_PAD_GPIO1_IO03 register\n");
        iounmap(IMX6U_CCM_CCGR1);
        iounmap(SW_MUX_GPIO1_IO03);
        return -ENOMEM;
    }
    
    GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
    if (!GPIO1_DR)
    {
        printk("Failed to map GPIO1_DR register\n");
        iounmap(IMX6U_CCM_CCGR1);
        iounmap(SW_MUX_GPIO1_IO03);
        iounmap(SW_PAD_GPIO1_IO03);
        return -ENOMEM;
    }
    
    GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);
    if (!GPIO1_GDIR)
    {
        printk("Failed to map GPIO1_GDIR register\n");
        iounmap(IMX6U_CCM_CCGR1);
        iounmap(SW_MUX_GPIO1_IO03);
        iounmap(SW_PAD_GPIO1_IO03);
        iounmap(GPIO1_DR);
        return -ENOMEM;
    }

    /* 2. Enable GPIO1 clock (bits 27:26 = 11) */
    val = readl(IMX6U_CCM_CCGR1);
    val &= ~(3 << 26);         /* Clear bits 27:26 */
    val |= (3 << 26);          /* Set bits 27:26 to enable clock */
    writel(val, IMX6U_CCM_CCGR1);

    /* 3. Configure GPIO1_IO03 as GPIO function (value 5) */
    writel(5, SW_MUX_GPIO1_IO03);

    /* Set IO properties for register SW_PAD_GPIO1_IO03
     * bit 16:0 HYS disabled
     * bit [15:14]: 00 default pull down
     * bit [13]: 0 keeper function
     * bit [12]: 1 pull/keeper enabled
     * bit [11]: 0 open drain disabled
     * bit [7:6]: 10 speed 100Mhz
     * bit [5:3]: 110 R0/6 drive strength
     * bit [0]: 0 low slew rate
     */
    writel(0x10B0, SW_PAD_GPIO1_IO03);

    /* 4. Set GPIO1_IO03 as output (bit 3 = 1) */
    val = readl(GPIO1_GDIR);
    val &= ~(1 << 3);          /* Clear bit 3 */
    val |= (1 << 3);           /* Set bit 3 = output */
    writel(val, GPIO1_GDIR);

    /* 5. Initialize LED off (bit 3 = 1 = high = LED off) */
    val = readl(GPIO1_DR);
    val |= (1 << 3);           /* Set bit 3 = LED off */
    writel(val, GPIO1_DR);

    /* Register character device driver */
    /* 1. Create device number */
    if (newchrled.major)
    {
        newchrled.devid = MKDEV(newchrled.major, 0);
        register_chrdev_region(newchrled.devid, NEWCHRLED_CNT, NEWCHRLED_NAME);
    }
    else
    {
        alloc_chrdev_region(&newchrled.devid, 0, NEWCHRLED_CNT, NEWCHRLED_NAME);  /* Allocate device number */
        newchrled.major = MAJOR(newchrled.devid);     /* Get allocated major device number */
        newchrled.minor = MINOR(newchrled.devid);     /* Get allocated minor device number */
    }
    printk("newcheled major=%d,minor=%d\r\n", newchrled.major, newchrled.minor);

    /* 2. Initialize character device structure */
    newchrled.cdev.owner = THIS_MODULE;
    cdev_init(&newchrled.cdev, &newchrled_fops);

    /* 3. Add character device to kernel */
    cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_CNT);

    /* 4. Create device class */
    newchrled.class = class_create(THIS_MODULE, NEWCHRLED_NAME);
    if (IS_ERR(newchrled.class))
    {
        return PTR_ERR(newchrled.class);
    }

    /* 5. Create device node in /dev/ */
    newchrled.device = device_create(newchrled.class, NULL, newchrled.devid, NULL, NEWCHRLED_NAME);
    if (IS_ERR(newchrled.device))
    {
        return PTR_ERR(newchrled.device);
    }

    return 0;
}

/**
 * @brief Driver exit function
 * @param None
 * @return None
 */
static void __exit led_exit(void)
{
    /* Unmap register virtual addresses */
    iounmap(IMX6U_CCM_CCGR1);
    iounmap(SW_MUX_GPIO1_IO03);
    iounmap(SW_PAD_GPIO1_IO03);
    iounmap(GPIO1_DR);
    iounmap(GPIO1_GDIR);

    /* Unregister character device driver */
    /* Destroy device node first, then remove cdev */
    if (newchrled.device)
    {
        device_destroy(newchrled.class, newchrled.devid);
    }
    
    if (newchrled.class)
    {
        class_destroy(newchrled.class);
    }
    
    cdev_del(&newchrled.cdev);                     /* Remove cdev from kernel */
    unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT);  /* Free device number */
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("yushuang");